package com.xhdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.xhdp.utils.RedisConstants.CACHE_NULL_TTL;
import static com.xhdp.utils.RedisConstants.LOCK_DEFAULT_TTL;

/**
 * redis缓存工具类
 */
@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;
    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 为热点key设置逻辑过期
     * @param key 键
     * @param value 值
     * @param expire 逻辑过期时间
     * @param unit 逻辑过期时间类型
     */
    public void setWithLogicalExpire(String key, Object value, Long expire, TimeUnit unit) {
        //设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(expire)));
        //写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 缓存空值解决根据id查询数据的 缓存穿透问题（redis缓存和数据库都不存在该数据时发生）
     * @param prefix key前缀
     * @param id 数据id
     * @param type 方法返回值类型（查询数据类型）
     * @param dbFallback 查询数据库方法
     * @param time 重建缓存过期时间
     * @param unit 过期时间类型
     * @return 返回正确数据或者null
     * @param <T> 方法返回值类型
     * @param <ID> 传入id类型
     */
    public <T, ID> T queryWithPassThrough(String prefix, ID id, Class<T> type,
                                          Function<ID, T> dbFallback, Long time, TimeUnit unit) {
        String key = prefix + id;
        //从redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //判断缓存是否存在
        if (StrUtil.isNotBlank(json)) {
            //存在缓存，直接返回数据
            return JSONUtil.toBean(json, type);
        }
        //判断命中的是否为空值
        if (json != null) {
            return null;
        }
        //不存在缓存，查询数据库
        T t = dbFallback.apply(id);
        //不存在数据
        if (t == null) {
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            //返回错误信息
            return null;
        }
        //存在数据，添加到redis缓存
        this.set(key, t, time, unit);
        //返回数据
        return t;
    }

    //创建线程池
    private static final ExecutorService
            CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 逻辑过期解决根据id查询数据的 缓存击穿问题（热点key突然过期，因重建耗时长被高并发访问时发生）
     * @param prefix key前缀
     * @param id 数据id
     * @param type 方法返回值类型（查询数据类型）
     * @param dbFallback 查询数据库方法
     * @param expire 重建缓存逻辑过期时间
     * @param unit 过期时间类型
     * @param lockPrefix 互斥锁前缀
     * @return 返回正确数据、旧数据或者null
     * @param <T> 方法返回值类型
     * @param <ID> 传入id类型
     */
    public <T, ID> T queryWithLogicalExpire(String prefix, ID id, Class<T> type, Function<ID, T> dbFallback,
                                            Long expire, TimeUnit unit, String lockPrefix) {
        String key = prefix + id;
        //从redis中查询缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        //判断缓存是否存在
        if (StrUtil.isBlank(json)) {
            //未命中，返回空
            return null;
        }
        //命中，取出数据
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        T t = BeanUtil.toBean(redisData.getData(), type);
        //判断是否逻辑过期
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            //未过期，返回店铺信息
            return t;
        }
        //已过期，缓存重建
        String lockKey = lockPrefix + id;
        //获取锁
        boolean isLock = tryLock(lockKey);
        if (isLock) {
            //获取到锁，doubleCheck
            json = stringRedisTemplate.opsForValue().get(key);
            if (StrUtil.isBlank(json)) {
                return null;
            }
            redisData = JSONUtil.toBean(json, RedisData.class);
            t = BeanUtil.toBean(redisData.getData(), type);
            LocalDateTime newExpireTime = redisData.getExpireTime();
            if (newExpireTime.isAfter(LocalDateTime.now())) {
                return t;
            }
            //过期，开启独立线程，重建缓存
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //查询数据库
                    T value = dbFallback.apply(id);
                    //模拟缓存击穿
                    Thread.sleep(200);
                    //重建缓存
                    this.setWithLogicalExpire(key, value, expire, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //返回过期数据
        return t;
    }

    /**
     * 获取锁 redis使用setnx模拟实现互斥锁(setnx 只有key不存在时才能写入)
     * @param key
     * @return
     */
    public boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(key, "1", LOCK_DEFAULT_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁 redis使用setnx模拟实现互斥锁(setnx 只有key不存在时才能写入)
     * @param key
     */
    public void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

}
